29  农产品交易销售趋势与产品结构分析

29.1 引言深度分析目标

  1. 销售趋势分析: 时间规律、季节性、周期性
  2. 产品结构分析: 市场格局、价格段、季节性差异

29.2 本章学习目标

本章在第28章探索性可视化的基础上,对同一份农产品交易数据做趋势与结构两个方向的深度分析。通过本章学习,你将掌握:

  1. 日度、月度双粒度的销售趋势统计与可视化(折线图+柱状图)
  2. rolling(window=7) 移动平均平滑日度波动、凸显趋势
  3. 用季度透视表加堆叠柱状图刻画品类结构及其演变
  4. pd.cut 划分价格段并统计订单数、销售额占比
  5. 用”日期转数值+线性回归”做简单趋势外推,并认识其局限

先修内容:第 章节 13 章(分组聚合与透视)与第 章节 20 章(常用图形绘制);线性回归的建模流程可对照第 章节 22 章。

29.3 销售趋势分析

列表 29.1: 销售趋势分析
# 注:processed_data.csv数据文件本地没有,但平台已经内置
# 注:本代码块假设全局变量 data(processed_data.csv)已由平台任务代码块加载

import pandas as pd  # 导入Pandas数据分析库
import matplotlib.pyplot as plt  # 导入Matplotlib绘图库
from datetime import datetime, timedelta
from matplotlib.dates import MonthLocator, DateFormatter  # 导入Matplotlib库
from sklearn.linear_model import LinearRegression  # 导入Scikit-learn的LinearRegression模块

def sales_trend_analysis(orders):  # 销售趋势分析函数
    data['下单日期'] = pd.to_datetime(data['order_date']).dt.date  # 转换为日期格式
    
    # 按日期统计订单数和销售额
    daily_sales = data.groupby('下单日期').agg({'order_id':'count','sales_amount':'sum'}).reset_index()
    daily_sales.columns = ['日期', '订单数', '销售额']  # 重命名列
    
    # 计算7日移动平均线(平滑波动)
    daily_sales['订单数_7日均值'] = daily_sales['订单数'].rolling(window=7, min_periods=1).mean()
    daily_sales['销售额_7日均值'] = daily_sales['销售额'].rolling(window=7, min_periods=1).mean()  # 设置滚动窗口计算
    
    # 可视化每日销售数据
    fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(18, 14), sharex=True)
    
    # 订单数趋势图
    ax1.plot(daily_sales['日期'], daily_sales['订单数'], 'b-', alpha=0.3, label='每日订单数')
    ax1.plot(daily_sales['日期'], daily_sales['订单数_7日均值'], 'r-', label='7日移动平均')  # 在子图上绘制曲线
    ax1.set_title('每日订单量趋势')  # 设置图表标题
    ax1.set_ylabel('订单数量')  # 设置Y轴标签
    ax1.legend()  # 设置图例
    ax1.grid(True)  # 设置网格线
    
    # 销售额趋势图
    ax2.plot(daily_sales['日期'], daily_sales['销售额'], 'g-', alpha=0.3, label='每日销售额')
    ax2.plot(daily_sales['日期'], daily_sales['销售额_7日均值'], 'r-', label='7日移动平均')  # 在子图上绘制曲线
    ax2.set_title('每日销售额趋势')  # 设置图表标题
    ax2.set_xlabel('日期')  # 设置X轴标签
    ax2.set_ylabel('销售额(元)')  # 设置Y轴标签
    ax2.legend()  # 设置图例
    ax2.grid(True)  # 设置网格线
    
    # 设置日期显示格式
    ax2.xaxis.set_major_locator(MonthLocator())
    ax2.xaxis.set_major_formatter(DateFormatter('%Y-%m'))  # 配置子图属性
    plt.tight_layout()  # 自动调整布局防止重叠
    plt.savefig("每日销售趋势.png")  # 保存图表
    
    # 月度销售趋势分析
    data['年月'] = pd.to_datetime(data['order_date']).dt.strftime('%Y-%m')  # 提取年月
    
    # 按月统计订单数和销售额
    monthly_sales = data.groupby('年月').agg({'order_id':'count','sales_amount':'sum'}).reset_index()
    monthly_sales.columns = ['年月', '订单数', '销售额']  # 重命名列
    
    # 可视化月度销售数据
    fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(16, 12))
    
    # 月度订单数柱状图
    ax1.bar(monthly_sales['年月'], monthly_sales['订单数'], color='skyblue')
    ax1.set_title('月度订单数')  # 设置图表标题
    ax1.set_ylabel('订单数量')  # 设置Y轴标签
    # 设置X轴刻度标签水平显示(旋转角度为0度)
    plt.setp(ax1.xaxis.get_majorticklabels(), rotation=0)
    ax1.grid(axis='y')  # 设置网格线
    for i, v in enumerate(monthly_sales['订单数']):  # 添加数值标签
        ax1.text(i, v + 10, str(v), ha='center', va='bottom', fontsize=10)  # 配置子图属性
    
    # 月度销售额柱状图
    ax2.bar(monthly_sales['年月'], monthly_sales['销售额'], color='lightgreen')
    ax2.set_title('月度销售额')  # 设置图表标题
    ax2.set_xlabel('年月')  # 设置X轴标签
    ax2.set_ylabel('销售额(元)')  # 设置Y轴标签
    # 设置X轴刻度标签水平显示(旋转角度为0度)
    plt.setp(ax2.xaxis.get_majorticklabels(), rotation=0)
    ax2.grid(axis='y')  # 设置网格线
    for i, v in enumerate(monthly_sales['销售额']):  # 添加数值标签
        ax2.text(i, v + 1000, f"{v:.2f}", ha='center', va='bottom', fontsize=10)  # 配置子图属性
    plt.tight_layout()  # 自动调整布局防止重叠
    plt.savefig("月度销售数据.png")  # 保存图表
    
    # 季度商品大类销售额分析
    data['order_date'] = pd.to_datetime(data['order_date'])  # 转换日期格式
    data['年季'] = data['order_date'].dt.year.astype(str) + '-Q' + data['order_date'].dt.quarter.astype(str)  # 提取年季
    
    # 按年季和产品类型统计销售额
    quarterly_category_sales = data.groupby(['年季', 'category'])['sales_amount'].sum().reset_index()
    
    # 转换为透视表便于绘图
    quarterly_pivot = quarterly_category_sales.pivot(index='年季', columns='category', values='sales_amount').fillna(0)
    
    # 可视化季度商品销售额
    ax = quarterly_pivot.plot(kind='bar', stacked=True, figsize=(16,10), colormap='viridis')
    ax.set_title('季度各品类销售额', fontsize=16)  # 设置图表标题
    ax.set_xlabel('年-季度', fontsize=14)  # 设置X轴标签
    ax.set_ylabel('销售额(元)', fontsize=14)  # 设置Y轴标签
    ax.legend(title='商品大类', bbox_to_anchor=(1.05, 1), loc='upper left')  # 设置图例
    plt.xticks(rotation=0)  # 设置X轴刻度标签
    ax.grid(axis='y')  # 设置网格线
    plt.tight_layout()  # 自动调整布局防止重叠
    plt.savefig("季度品类销售额.png")  # 保存图表

    # 销售额预测(简单线性趋势)
    daily_sales['日期_数值'] = (pd.to_datetime(daily_sales['日期']) - pd.to_datetime(daily_sales['日期'].min())).dt.days  # 日期转数值
    
    # 准备特征和目标变量
    X = daily_sales[['日期_数值']]
    y_orders = daily_sales['订单数']  # 提取订单数列作为y_orders变量
    y_sales = daily_sales['销售额']  # 提取销售额列作为y_sales变量
    
    # 训练线性回归模型
    model_orders = LinearRegression()
    model_sales = LinearRegression()  # 初始化线性回归模型
    model_orders.fit(X, y_orders)  # 在数据上训练model_orders模型
    model_sales.fit(X, y_sales)  # 在数据上训练model_sales模型
    
    # 预测未来30天
    last_date = pd.to_datetime(daily_sales['日期'].max())
    future_dates = [last_date + timedelta(days=i) for i in range(1, 31)]  # 生成未来日期
    future_df = pd.DataFrame({'日期': future_dates})  # 创建数据框future_df
    future_df['日期_数值'] = (pd.to_datetime(future_df['日期']) - pd.to_datetime(daily_sales['日期'].min())).dt.days  # 未来日期转数值
    
    # 预测订单数和销售额
    future_df['订单数预测'] = model_orders.predict(future_df[['日期_数值']])
    future_df['销售额预测'] = model_sales.predict(future_df[['日期_数值']])  # 使用模型进行预测
    
    # 显示预测结果
    print("未来30天销售预测:")
    print((future_df.head(10)))  # 输出前几行数据
    
    # 可视化预测结果
    plt.figure(figsize=(18, 10))
    plt.plot(pd.to_datetime(daily_sales['日期']), daily_sales['销售额'], 'b-', alpha=0.5, label='历史销售额')  # 历史数据
    plt.plot(future_df['日期'], future_df['销售额预测'], 'r--', label='销售额预测')  # 预测数据
    plt.axvspan(last_date, future_df['日期'].max(), alpha=0.2, color='gray')  # 标记预测区间
    plt.title('销售额趋势与预测')  # 设置图表标题
    plt.xlabel('日期')  # 设置X轴标签
    plt.ylabel('销售额(元)')  # 设置Y轴标签
    plt.legend()  # 添加图例
    plt.grid(True)  # 显示网格线
    plt.tight_layout()  # 自动调整布局防止重叠
    plt.savefig("销售额预测.png")  # 保存图表
    print("销售趋势分析完成")  # 输出销售趋势分析完成

# 进行销售趋势分析
sales_trend_analysis(data)

任务要求:读取平台内置的 processed_data.csv,完成产品名到品类的映射与季节特征构造(月份→四季)后,定义 product_analysis 函数完成产品结构分析——按商品大类统计订单数、销售额与平均订单金额并绘制柱状图;统计商品子类并可视化销售额Top3;用 pd.cut 把价格划分为0-100、100-200、200-500元三段,统计各价格段订单数与销售额占比并双子图绘制;用 squarify 绘制品类销售额树形图;按季节×品类透视计算销售占比并绘制热力图;最后调用该函数执行全部分析。

平台任务(平台原始代码)

以下为平台原始代码(含已知缩进问题,见块内注记):

列表 29.2: 平台原始代码
# 注:该代码块存在缩进错误,请按平台原始题面原样输入,以平台判定为准;processed_data.csv数据文件本地没有,但平台已经内置;squarify包本地未安装,但平台已经内置
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
# 注:该代码块存在缩进错误,请按平台原始题面原样输入,以平台判定为准
import pandas as pd  # 导入Pandas数据分析库
import numpy as np  # 导入NumPy数值计算库
import matplotlib.pyplot as plt  # 导入Matplotlib绘图库
import seaborn as sns  # 导入Seaborn可视化库
import warnings  # 导入warnings模块用于控制警告输出
warnings.filterwarnings('ignore')  # 忽略警告
plt.rcParams['font.sans-serif'] = ['SimHei']  # 中文显示
plt.rcParams['axes.unicode_minus'] = False  # 负号显示
data = pd.read_csv('processed_data.csv')  # 读取数据

# 产品分类映射
category_mapping = {'安溪铁观音':'茶叶','武夷岩茶':'茶叶','福州茉莉花':'茶叶','古田银耳':'食用菌','建宁莲子':'中药材','琯溪蜜柚':'水果','宁德大黄鱼':'水产品'}
data['category'] = data['product_name'].map(category_mapping).fillna(data['category'])  # 更新产品分类
print(data.head())  # 查看数据

# 产品分析
import squarify  # 导入树形图库
# 创建季节特征(复用之前的季节映射)
seasons = {1: '冬季', 2: '冬季', 3: '春季', 4: '春季', 5: '春季', 6: '夏季', 7: '夏季', 8: '夏季', 9: '秋季', 10: '秋季', 11: '秋季', 12: '冬季'}
data['下单月份'] = pd.to_datetime(data['order_date']).dt.month  # 提取月份
data['order_season'] = data['下单月份'].map(seasons)  # 映射季节

def product_analysis(orders):  # 产品分析函数
    # 按商品大类统计销售数据
    category_stats = data.groupby('category').agg({'order_id':'count','sales_amount':'sum'}).reset_index()  #【要求1】
    category_stats.columns = ['产品类型', '订单数', '销售额']  # 重命名列
    category_stats['平均订单金额'] = category_stats['销售额'] / category_stats['订单数']  # 计算平均订单金额
    category_stats = category_stats.sort_values('销售额', ascending=False)  # 按销售额排序
    
    # 显示商品大类统计
    print("产品类型统计:")
        print(category_stats)  # 输出统计量数据
    
    # 可视化商品大类销售额
    plt.figure(figsize=(14, 8))
    sns.barplot(x='产品类型', y='销售额', data=category_stats, palette='viridis')  # 绘制分类柱状图
    plt.title('各商品大类销售额')  # 设置图表标题
    plt.xlabel('产品类型')  # 设置X轴标签
    plt.ylabel('销售额(元)')  # 设置Y轴标签
    plt.grid(axis='y')  # 显示网格线
    plt.tight_layout()  # 自动调整布局防止重叠
    plt.savefig("商品大类销售额.png")  # 保存图表
    
    # 按商品子类统计销售数据
    subcategory_stats = data.groupby(['category', 'product_name']).agg({'order_id':'count','sales_amount':'sum'}).reset_index()  
    subcategory_stats.columns = ['产品类型', '产品子类', '订单数', '销售额']  # 重命名列
    subcategory_stats['平均订单金额'] = subcategory_stats['销售额'] / subcategory_stats['订单数']  # 计算平均订单金额
    subcategory_stats = subcategory_stats.sort_values('销售额', ascending=False)  # 按销售额排序
    
    # 显示Top3商品子类
    print("产品子类统计:")
    print(subcategory_stats.head(3))  # 输出前几行数据
    
    # 可视化Top3商品子类销售额
    top_subcategories = subcategory_stats.head(3)
    plt.figure(figsize=(16, 10))  # 创建图形画布
    sns.barplot(x='销售额', y='产品子类', hue='产品类型', data=top_subcategories, palette='viridis')  # 绘制分类柱状图
    plt.title('各产品子类销售额Top 3')  # 设置图表标题
    plt.xlabel('销售额(元)')  # 设置X轴标签
    plt.ylabel('产品子类')  # 设置Y轴标签
    plt.grid(axis='x')  # 显示网格线
    plt.tight_layout()  # 自动调整布局防止重叠
    plt.savefig("Top3子类销售额.png")  # 保存图表
    
    # 商品定价分析
    data['价格段'] = pd.cut(data['price'], bins=[0, 100, 200, 500], labels=['0-100元', '100-200元', '200-500元'])  #【要求2】
    
    # 按价格段统计销售数据
    price_range_stats = data.groupby('价格段').agg({'order_id':'count','sales_amount':'sum'}).reset_index()
    price_range_stats.columns = ['价格段', '订单数', '销售额']  # 重命名列
    price_range_stats['订单占比'] = price_range_stats['订单数'] / price_range_stats['订单数'].sum() * 100  # 计算订单占比
    price_range_stats['销售额占比'] = price_range_stats['销售额'] / price_range_stats['销售额'].sum() * 100  # 计算销售额占比
    # 显示价格段统计
    print("价格段统计:")
        print(price_range_stats)  # 输出价格数据
    
    # 可视化价格段销售情况
    fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(16, 14))
    
    # 订单数占比柱状图
    sns.barplot(x='价格段', y='订单占比', data=price_range_stats, palette='Blues_d', ax=ax1)
    ax1.set_title('各价格段订单数占比')  # 设置图表标题
    ax1.set_xlabel('价格段')  # 设置X轴标签
    ax1.set_ylabel('订单数占比(%)')  # 设置Y轴标签
    ax1.grid(axis='y')  # 设置网格线
    
    # 销售额占比柱状图
    sns.barplot(x='价格段', y='销售额占比', data=price_range_stats, palette='Reds_d', ax=ax2)
    ax2.set_title('各价格段销售额占比')  # 设置图表标题
    ax2.set_xlabel('价格段')  # 设置X轴标签
    ax2.set_ylabel('销售额占比(%)')  # 设置Y轴标签
    ax2.grid(axis='y')  # 设置网格线
    plt.tight_layout()  # 自动调整布局防止重叠
    plt.savefig("价格段销售占比.png")  # 保存图表
    
    # 商品销售分布树形图
    plt.figure(figsize=(16, 12))
    category_sales = category_stats[['产品类型', '销售额']].copy()  # 创建数据副本
    category_sales['标签'] = category_sales['产品类型'] + '\n' + category_sales['销售额'].apply(lambda x: f'{x:,.0f}元')  # 生成标签
    squarify.plot(sizes=category_sales['销售额'], label=category_sales['标签'], alpha=0.8, color=sns.color_palette("viridis", len(category_sales)))  # 绘制树形图
        plt.axis('off')  # 隐藏坐标轴
    plt.title('各产品类型销售额树形图', fontsize=18)  # 设置图表标题
    plt.tight_layout()  # 自动调整布局防止重叠
    plt.savefig("产品销售额树形图.png")  # 保存图表
    
    # 商品季节性分析
    seasonal_product = data.groupby(['order_season', 'category'])['sales_amount'].sum().reset_index()  # 按季节和产品类型统计销售额
    season_pivot = seasonal_product.pivot(index='category', columns='order_season', values='sales_amount')  # 【要求3】
    season_pct = season_pivot.div(season_pivot.sum(axis=1), axis=0) * 100  # 计算各季节占比
    
    # 显示季节性销售占比
    print("季节商品销售占比(%):")
        print(season_pct)  # 输出百分比数据
    
    # 可视化季节性销售占比
    plt.figure(figsize=(14, 10))
    sns.heatmap(season_pct, annot=True, fmt='.1f', cmap='YlGnBu')  # 绘制热力图
    plt.title('各产品类型在不同季节的销售占比(%)')  # 设置图表标题
    plt.xlabel('季节')  # 设置X轴标签
    plt.ylabel('产品类型')  # 设置Y轴标签
    plt.tight_layout()  # 自动调整布局防止重叠
    plt.savefig("产品季节销售占比.png")  # 保存图表
    print("产品分析完成")  # 输出产品分析完成

# 进行产品分析
product_analysis(data)

预期输出:数据文件平台内置,本地无数据,具体数值以平台运行结果为准;该代码块含平台题面原有的缩进错误,请按平台原始题面原样输入,以平台判定为准。判读要点:

  1. 文本输出:数据前几行;产品类型统计表(订单数、销售额、平均订单金额);Top3产品子类表;价格段统计表(含订单占比与销售额占比);季节×品类销售占比表。
  2. 图形输出(商品大类销售额.png、Top3子类销售额.png):看品类量级排序与断层、头部单品归属的品类。
  3. 图形输出(价格段销售占比.png):双子图分别看订单占比与销售额占比——若订单集中于低价段而销售额并非如此,说明高价段单品贡献了不成比例的金额。
  4. 图形输出(产品销售额树形图.png):矩形面积即品类销售额占比,与柱状图互证;图形输出(产品季节销售占比.png):看哪些品类的销售明显集中于个别季节(该热力图已按品类内部归一化)。
  5. squarify 包平台已内置、本地未安装,该图本地无法复现,以平台运行结果为准。

29.4 产品结构分析

列表 29.3
# 注:该代码块依赖的数据来自上方平台任务代码块,本块承接上方平台任务的代码与数据(变量沿用平台任务)

# ==================== 按品类统计销售数据 ====================
category_sales = data.groupby('category')['sales_amount'].sum().sort_values(ascending=False)
# 按产品类别分组,对销售额求和,然后按销售额降序排序
# sort_values(ascending=False)表示降序排列

# ==================== 创建柱状图 ====================
plt.figure(figsize=(12, 8))  # 创建12x8英寸的画布
bars = plt.bar(category_sales.index, category_sales.values, color='steelblue', alpha=0.7)
# 绘制柱状图,x轴为品类,y轴为销售额

# ==================== 添加数值标签 ====================
for bar in bars:  # 遍历每个柱子
    height = bar.get_height()  # 获取柱子高度(销售额)
    plt.text(
        bar.get_x() + bar.get_width() / 2.,  # x坐标为柱子中心
        height,  # y坐标为柱子顶部
        f'{height/10000:.1f}万',  # 显示销售额,转换为万元单位,保留1位小数
        ha='center',  # 水平居中对齐
        va='bottom',  # 垂直底部对齐
        fontsize=11  # 字体大小
    )

plt.title('各品类销售总额', fontsize=16)  # 设置图表标题
plt.xlabel('产品类别', fontsize=12)  # 设置x轴标签
plt.ylabel('销售金额(元)', fontsize=12)  # 设置y轴标签
plt.grid(axis='y', alpha=0.3)  # 显示y轴网格线
plt.tight_layout()  # 自动调整布局
plt.show()  # 显示图表

29.5 分析结论

  1. 销售趋势: 存在明显的周期性和季节性
  2. 核心品类: 茶叶是主要销售品类
  3. 移动平均: 平滑了短期波动,更易观察趋势
  4. 业务洞察: 需根据季节性调整库存和营销

29.6 本章小结

本章新增的技能要点:

  1. 趋势分析三件套:日度原始序列+7日移动平均、月度柱状图、季度堆叠柱状图,粒度由细到粗互相印证。
  2. 移动平均是平滑工具:rolling(window=7, min_periods=1).mean() 抑制日度噪声;窗口越长越平滑,但对近期变化越迟钝。
  3. 结构分析用”透视+占比”:季度×品类透视后按品类内部归一化(div(sum(axis=1), axis=0)),热力图才能公平比较体量悬殊的品类。
  4. pd.cut 分箱:价格段边界与标签必须业务可解释,分箱方式改变会直接改变占比结论。
  5. 简单趋势外推的边界:“日期转数值+线性回归”只捕捉平均斜率,不含季节性与促销脉冲,只宜作基准情景。

易错点:

  • pd.cut(bins=[0, 100, 200, 500]) 会把超过500元的订单挤出箱外:它们不计入占比分母,右端宜开到 np.inf;分箱边界一变占比结论随之改变,新旧口径不能混用
  • 季节热力图按全表归一:颜色最深的格子几乎必然属于体量最大的品类,小品类季节集中被淹没;比较季节形态应按品类内部归一化,即 div(sum(axis=1), axis=0)
  • rolling(window=7) 省略 min_periods=1:序列开头不足7天的位置是 NaN,并非数据缺失;窗口取30时拐点要推迟约半个窗口才显现,别用它判断刚发生的下滑
  • 把”日期转数值+线性回归”的30天外推当事实汇报:模型只含平均斜率、没有季节项,预测与历史旺季矛盾时,第一嫌疑是模型结构而非市场转向
  • 平台任务代码块含题面原有的缩进错误(多处 print 缩进异常):不要顺手修正后再提交,平台只认原始题面
  • 本地缺 squarify 导致树形图跑不通:该包平台已内置,以平台运行结果为准,不是代码写错

29.7 动手与思考

以下练习每题附参考答案(默认折叠);概念题与变式题可对照自查,商业判断题不设唯一答案,判断依据与口径比结论更重要。

  1. 概念辨析:7日移动平均与原始日序列各自回答什么问题?窗口取7与取30,读图结论会有什么差异?

    参考答案(点开前请先独立完成)

    解题思路:原始日序列回答“具体某一天发生了什么”——异常日、促销脉冲、节假日缺口都只在原始序列上可见;7日移动平均回答“剔除周内波动之后,趋势朝哪个方向走”。窗口取7恰好覆盖一个星期,能消除工作日与周末的周期性差异,同时保留中期起伏;窗口取30(约一个月)进一步抹平月内波动,曲线更平滑,但对近期变化的反应变迟钝——趋势拐点会被推迟约半个窗口的长度才显现,刚发生的下滑在30日线上可能还看不出来。另有一个技术差别值得知道:平台代码写的是 rolling(window=7, min_periods=1)min_periods=1 让序列开头就有值(前6天是可用数据的部分均值);若省略该参数,开头不足7天的位置是缺失值(本地实测:前者首日就有均值,后者前6天为NaN)。

    回扣本章:对应“本章新增的技能要点”第2条(移动平均是平滑工具:窗口越长越平滑,但对近期变化越迟钝)。

  2. 概念辨析:季节占比为何按”每个品类内部归一化”而不是全表归一?两种口径的热力图分别适合回答什么问题?

    参考答案(点开前请先独立完成)

    解题思路:按品类内部归一化(平台代码 season_pivot.div(season_pivot.sum(axis=1), axis=0),每行除以该行合计)之后,每个品类的四个季节占比合计为100%,品类的体量信息被完全消去。这样做的理由是:不同品类销售额可能相差一个量级,若全表归一(所有格子除以全表合计),颜色最深的格子几乎必然属于体量最大的品类,小品类即使销售高度集中在某一季,颜色也很浅——体量淹没了季节形态。两种口径各有所长:内部归一化回答“每个品类的销售在四季之间怎么分配、哪个品类最依赖哪个季节”,大小品类的季节形态可以直接对比;全表归一回答“哪个品类×哪个季节组合贡献了最多的销售额”,保留体量信息,适合把资源投向最大组合的决策。用哪张图,取决于问题是“形态对比”还是“贡献结构”。

    回扣本章:对应“本章新增的技能要点”第3条(结构分析用“透视+占比”,归一化方向就是口径)。

  3. 变式任务:把价格段边界改为0-50、50-150、150-300、300元以上四档,占比结论应如何重新表述?分箱变化会影响哪些图形?

    参考答案(点开前请先独立完成)

    改造思路:只改 pd.cut 一行——边界由 [0, 100, 200, 500] 改为 [0, 50, 150, 300, np.inf],标签同步换成四档(右端用 np.inf,使超过300元的订单全部落入“300元以上”,避免像原三档那样把超过500元的订单挤出箱外、不计入占比分母);后续统计与绘图代码不变,自动按新四档重新汇总。占比结论不能只改数字,要整段重述:原“0-100元”档被拆成“0-50元”与“50-150元”两段(原档占比约等于新两档占比之和),高价端边界由500元移到300元。分箱变化只影响价格段统计表与“价格段销售占比.png”这一组图形;商品大类柱状图、Top3子类图、树形图、季节热力图与趋势图都不以价格段为分组,不受影响。

    # 变式代码:价格段边界由三档改为四档(只改pd.cut一行,统计与绘图行不变)
    import numpy as np  # 导入NumPy数值计算库(np.inf表示右端开到无穷)
    data['价格段'] = pd.cut(data['price'], bins=[0, 50, 150, 300, np.inf], labels=['0-50元', '50-150元', '150-300元', '300元以上'])  # ← 改动点:四档边界与标签
    price_range_stats = data.groupby('价格段').agg({'order_id': 'count', 'sales_amount': 'sum'}).reset_index()  # 与平台任务一致
    price_range_stats.columns = ['价格段', '订单数', '销售额']  # 与平台任务一致
    price_range_stats['订单占比'] = price_range_stats['订单数'] / price_range_stats['订单数'].sum() * 100  # 与平台任务一致
    price_range_stats['销售额占比'] = price_range_stats['销售额'] / price_range_stats['销售额'].sum() * 100  # 与平台任务一致
    print(price_range_stats)  # 与平台任务一致的输出,数值按新四档重新汇总

    结构性判读:统计表仍是“价格段×四列(订单数、销售额、订单占比、销售额占比)”,只是行由3行变为4行;双子图各多一根柱。判读要点:先核对四行订单占比之和是否为100%(分母只计落在箱内的订单),再对照原三档表——低价段的内部结构被拆开、高价端边界后移,订单占比与销售额占比的集中档是否随之换位;结论表述必须注明“四档口径下”,避免与三档口径的旧结论混用。

    本地演练版(模拟数据):在20笔模拟订单价格上实跑两种分箱:

    # 本地演练版:原三档与新四档两种分箱的占比对照
    import pandas as pd  # 导入Pandas数据分析库
    import numpy as np  # 导入NumPy数值计算库
    prices = pd.Series([45, 80, 120, 160, 220, 280, 350, 420, 95, 60, 130, 240, 310, 55, 75, 105, 175, 260, 330, 480])  # 模拟20笔订单价格
    old_bins = pd.cut(prices, bins=[0, 100, 200, 500], labels=['0-100元', '100-200元', '200-500元'])  # 平台原三档
    new_bins = pd.cut(prices, bins=[0, 50, 150, 300, np.inf], labels=['0-50元', '50-150元', '150-300元', '300元以上'])  # 变式四档
    old_stat = old_bins.value_counts().sort_index()  # 原口径各档订单数
    new_stat = new_bins.value_counts().sort_index()  # 新口径各档订单数
    print('原三档订单数与占比:')
    print(pd.DataFrame({'订单数': old_stat, '占比': (old_stat / old_stat.sum()).round(3)}))  # 占比分母为落在箱内的订单数
    print('新四档订单数与占比:')
    print(pd.DataFrame({'订单数': new_stat, '占比': (new_stat / new_stat.sum()).round(3)}))  # 同一批订单重新分档
    print('原“0-100元”档被新边界拆分到:')
    print(pd.crosstab(old_bins, new_bins).iloc[0])  # 原低档订单在新四档中的去向

    模拟数据演练结果(本机 peter 环境实际运行结果,仅演示流程与判读方法,真实数值以平台运行结果为准):

    原三档订单数与占比:
              订单数    占比
    0-100元      6  0.30
    100-200元    5  0.25
    200-500元    9  0.45
    新四档订单数与占比:
              订单数    占比
    0-50元       1  0.05
    50-150元     8  0.40
    150-300元    6  0.30
    300元以上      5  0.25
    原“0-100元”档被新边界拆分到:
    col_0
    0-50元       1
    50-150元     5
    150-300元    0
    300元以上      0
    Name: 0-100元, dtype: int64

    演练判读:原“0-100元”档的6笔订单有5笔落进“50-150元”、只有1笔留在“0-50元”——四档口径下“订单集中于50-150元(40%)”的表述,与三档口径“集中于200-500元(45%)”指向完全不同的价位带,同一批订单、两种结论。

    预期输出:数据文件由教学平台内置,本机无法复现,具体数值以平台运行结果为准。

    注意:以上为本题变式的独立代码;列表 29.2 对应平台任务的原始代码块仍须原样输入教学平台(该平台代码块含平台题面原有的缩进错误,按平台原始题面原样输入,以平台判定为准),不要用本变式替换。

    回扣本章:对应“本章新增的技能要点”第4条(pd.cut 分箱:边界与标签必须业务可解释,分箱方式改变会直接改变占比结论)。

  4. 变式任务:把线性趋势预测的展望期从30天改为90天,并考虑叠加季节性调整(思路:用历史各月占比修正外推值),改造路径是什么?

    参考答案(点开前请先独立完成)

    改造思路:分两处。展望期上,生成未来日期的那一行由 range(1, 31) 改为 range(1, 91),其余预测代码不变。季节调整上,先算历史各月销售额占比(data.groupby('下单月份')['sales_amount'].sum() 除以总额),除以均匀占比1/12得到季节系数(旺季大于1、淡季小于1),再把未来每个日期按其自然月匹配系数、乘到线性外推值上。

    # 变式代码:展望期30天→90天,并用历史各月占比修正外推值(只列改动与新增行)
    future_dates = [last_date + timedelta(days=i) for i in range(1, 91)]  # ← 改动点1:range(1, 31)改为range(1, 91)
    future_df = pd.DataFrame({'日期': future_dates})  # 与平台任务一致
    future_df['日期_数值'] = (pd.to_datetime(future_df['日期']) - pd.to_datetime(daily_sales['日期'].min())).dt.days  # 与平台任务一致
    future_df['订单数预测'] = model_orders.predict(future_df[['日期_数值']])  # 与平台任务一致
    future_df['销售额预测'] = model_sales.predict(future_df[['日期_数值']])  # 与平台任务一致
    month_share = data.groupby('下单月份')['sales_amount'].sum() / data['sales_amount'].sum()  # ← 新增:历史各月销售额占比
    season_factor = month_share / (1 / 12)  # ← 新增:以均匀占比1/12为基准的季节系数(旺季大于1、淡季小于1)
    future_df['自然月'] = pd.to_datetime(future_df['日期']).dt.month  # ← 新增:未来日期对应的自然月
    future_df['销售额预测_季节修正'] = future_df['销售额预测'] * future_df['自然月'].map(season_factor)  # ← 新增:外推值乘同月系数
    print(future_df[['日期', '销售额预测', '销售额预测_季节修正']].head(10))  # 两种预测并排对照

    结构性判读:预测表多出“销售额预测_季节修正”一列,图形上红色虚线段更长(约三个月)且在月份之间出现阶梯式起伏。判读要点:纯线性外推只含平均斜率,展望期越长与实际的偏离风险越大;季节修正在月与月之间重新分配量级,让旺季月份高于趋势线、淡季月份低于趋势线,但它仍是“趋势×历史季节形态”的基准情景,不含促销脉冲、年度增长变化与供给约束。

    本地演练版(模拟数据):在36个月“线性趋势×季节形态”的合成月度序列上实跑90天(9个月)展望与季节修正:

    # 本地演练版:90天展望并用历史同月占比修正线性外推(36个月合成月度数据)
    import numpy as np  # 导入NumPy数值计算库
    import pandas as pd  # 导入Pandas数据分析库
    from sklearn.linear_model import LinearRegression  # 导入线性回归模型
    rng = np.random.default_rng(7)  # 固定随机种子
    season_shape = np.tile([1.0, 1.1, 1.25, 1.05, 0.95, 1.0, 0.9, 0.95, 1.05, 1.15, 1.3, 1.2], 3)  # 模拟历史同月季节形态(冬春旺、盛夏淡)
    sales_month = (100 + 2.0 * np.arange(36)) * season_shape + rng.normal(0, 5, 36)  # 月度销售额=线性趋势×季节+噪声
    X_num = np.arange(36).reshape(-1, 1)  # 月份序号作回归特征(同平台“日期转数值”思路)
    model = LinearRegression().fit(X_num, sales_month)  # 只含趋势项的线性回归,同平台做法
    future_t = np.arange(36, 45).reshape(-1, 1)  # 改动点1:展望期由30天(约1个月)扩为90天(9个月)
    linear_pred = model.predict(future_t)  # 纯线性外推值
    month_frame = pd.DataFrame({'自然月': np.tile(np.arange(1, 13), 3), '销售额': sales_month})  # 汇总历史自然月
    season_factor = month_frame.groupby('自然月')['销售额'].mean() / month_frame['销售额'].mean()  # 新增:历史同月均值/总体均值=季节系数
    future_calendar = np.arange(1, 10)  # 未来9个月对应的自然月序号
    seasonal_pred = linear_pred * season_factor.reindex(future_calendar).to_numpy()  # 新增:外推值乘以同月系数
    print(pd.DataFrame({'未来月份': pd.period_range('2022-01', '2022-09', freq='M').astype(str), '纯线性外推': linear_pred.round(1), '季节修正后': seasonal_pred.round(1)}))  # 两种预测对照

    模拟数据演练结果(本机 peter 环境实际运行结果,仅演示流程与判读方法,真实数值以平台运行结果为准):

          未来月份  纯线性外推  季节修正后
    0  2022-01  184.8  160.6
    1  2022-02  187.0  179.3
    2  2022-03  189.3  205.3
    3  2022-04  191.5  181.0
    4  2022-05  193.8  165.5
    5  2022-06  196.0  180.4
    6  2022-07  198.3  161.7
    7  2022-08  200.5  182.6
    8  2022-09  202.8  200.4

    演练判读:纯线性外推每月只递增约2.2(趋势斜率),季节修正后3月(旺季)上修到205.3、7月(淡季)下修到161.7——修正让预测重新呈现出历史固有的月度起伏,这正是“趋势项+季节项”比单一趋势线更贴近业务节奏的地方。

    预期输出:数据文件由教学平台内置,本机无法复现,具体数值以平台运行结果为准。

    注意:以上为本题变式的独立代码;列表 29.2 对应平台任务的原始代码块仍须原样输入教学平台(该平台代码块含平台题面原有的缩进错误,按平台原始题面原样输入,以平台判定为准),不要用本变式替换。

    回扣本章:对应“本章新增的技能要点”第5条(“日期转数值+线性回归”只捕捉平均斜率,叠加季节系数是在承认这一边界上的补丁)。

  5. 商业判断:若预测曲线显示未来30天销售额趋势下行,而历史同季节通常是旺季(以平台运行结果为准),你如何向管理层解释这一矛盾,并给出备货与营销建议?

    参考答案(点开前请先独立完成)

    参考作答框架:本题不设唯一结论,优秀作答应覆盖以下维度。

    • 口径先行:先向管理层交代预测模型的口径——它是“日期转数值+线性回归”的趋势外推(列表 29.1 一节的做法),模型结构里根本没有季节项:季节性在拟合时被平均掉,线性斜率主要由近端数据走向决定。所谓“矛盾”,第一嫌疑是模型结构,而不是市场真的转向。
    • 解释框架(三个候选来源,逐一排查):其一,模型未含季节项,下行只是趋势项的读数;其二,若近端数月环比走弱(高基数、促销退坡、供给受限),斜率被拖低,外推自然下行;其三,历史旺季来自季节性而非趋势变化,两种成分在单一趋势线里无法区分。可用第4题的季节系数修正外推值,看修正后方向是否反转。
    • 备货与营销建议:以“季节调整后的趋势”作基准情景、纯线性外推作悲观情景;备货按基准情景加安全库存(农产品需计入保质期与损耗),营销资源保留弹性、按实际周环比滚动调整,而不是按单一情景一次性下注。
    • 风险考量:按线性下行过度砍库存会在旺季缺货、丢失全年利润大头;按历史旺季满备货则面临易腐品损耗与资金占压。两个方向的风险不对称,应由预测区间而非单点数值决定缓冲量。
    • 模型边界:简单外推不含促销脉冲、天气与供给冲击,预测期越长越不可信;汇报时应给出模型边界与情景假设,而不是一个“下行”的结论性数字。
    • 常见错误作答形态:把单一模型的输出当“事实”陈述、不交代模型不含季节项;或反过来全盘否定模型、凭经验拍一个备货量,同样没有数据依据。

    回扣本章:对应“本章新增的技能要点”第5条(简单趋势外推只宜作基准情景,使用前必须先认清它的边界)。